﻿
/// SDMX Artefact Tiny Repository. A repository for SAE Editor.
///    Copyright (C) 2010  Philippe Prati 
///
///    This program is free software: you can redistribute it and/or modify
///    it under the terms of the GNU General Public License as published by
///    the Free Software Foundation, either version 2 of the License, or
///    (at your option) any later version.
///
///    This program is distributed in the hope that it will be useful,
///    but WITHOUT ANY WARRANTY; without even the implied warranty of
///    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
///    GNU General Public License for more details.
///
///    You should have received a copy of the GNU General Public License
///    along with this program.  If not, see <http://www.gnu.org/licenses/>.
///
///    Contact: ph.prati@bluewin.ch
///    
using System;
using System.Xml;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Threading;
using System.Configuration;

/// <summary> Operations under mutex </summary>
public enum ListOperation
{
    Get,
    Update,
    Exist,
    CheckOut,
    CheckIn,
    SetDiffStatus,
    Delete
}

/// <summary>
/// This manager is a singleton usage
/// </summary>
public class ArtefactListMgr
{
    #region Lang Support

    private static string langConfigTag = "SupportedLanguages";
    private string[] _arrLang;

    private string[] GetSupportedLanguages()
    {
        string err = "Fatal Error: List of supported languages is missing or corrupted in configuration file.";
        if (_arrLang == null)
        {
            string supported = string.Empty;
            if (ConfigurationManager.AppSettings[langConfigTag] != null)
            {
                supported = ConfigurationManager.AppSettings[langConfigTag].Trim().ToLower();
                if (supported == null || supported.Length == 0)
                    throw new ApplicationException(err);
            }
            else
                // We assume fixe for the registry according to SFSO usage
                supported = "en;de;fr;it";

            _arrLang = supported.Split(';');
        }
        return _arrLang;
    }

    public IEnumerable<string> SupportedLanguages
    {
        get
        {
            foreach (string lg in _arrLang)
            {
                yield return lg;
            }
        }
    }

    public int LangCount { get { return GetSupportedLanguages().Length; } }

    #endregion

    #region Internal members

    // Create a new Mutex. The creating thread does not own the
    // Mutex.
    private static Mutex mut = new Mutex();

    // Artefact list document name
    private static string DirInfoFile = "dirInfo.sms";

    // A dataset table with artefact type key for each
    private Dictionary<string, DataSet> dicoList;

    /// <summary>Root directory of all artefact type sub directories</summary>
    private string _rootPath;

    #endregion //Internal members

    #region Properties

    /// <summary>Root directory of all artefact type sub directories</summary>
    public string RootPath
    {
        get { return _rootPath; }
    }

    #endregion //Properties

    #region Construction

    public ArtefactListMgr(string rootPath)
	{
        _rootPath = rootPath;
        if (_rootPath.EndsWith(new string(System.IO.Path.DirectorySeparatorChar, 1)) == false)
            _rootPath += System.IO.Path.DirectorySeparatorChar;

        // Ensure languages
        string[] arr = GetSupportedLanguages();
        if (arr.Length == 0)
            throw new ApplicationException("Unable to get languages from configuration!");

        // Create dictionnary for each artefact type 
        // (defined by sub directories of rootPath)
        dicoList = new Dictionary<string, DataSet>(16);

        DirectoryInfo diInfo = new DirectoryInfo(_rootPath);
        DirectoryInfo[] arArtDir = diInfo.GetDirectories();
        foreach (DirectoryInfo di in arArtDir)
        {
            dicoList.Add(di.Name.ToLower(), EnsureDataset(di));
        }
    }

    #endregion // Construction

    #region Load Artefact Lists

    private DataSet GetArtefactList(string artType)
    {
        string key = artType.ToLower();

        DataSet ds = null;
        if (dicoList.ContainsKey(key))
            ds = dicoList[key];

        return ds;
    }

    private DataSet EnsureDataset(DirectoryInfo di)
    {
        DataSet ds = new DataSet();

        // If listfile does not exists, create from scratch
        string path = di.FullName;
        if (path.EndsWith(new string(System.IO.Path.DirectorySeparatorChar, 1)) == false)
            path += System.IO.Path.DirectorySeparatorChar;

        if (System.IO.Directory.Exists(path))
        {
            if (System.IO.File.Exists(path + DirInfoFile))
            {
                // Read and adjusts if needed
                ds.ReadXml(path + DirInfoFile, XmlReadMode.ReadSchema);

                // Read old stuff
     //           if (ds.Tables.Count == 0)
     //               ds.ReadXml(path + DirInfoFile);
            }

            // Creates if needed
            if (ds == null || ds.Tables.Count == 0)
            {
                ds = new DataSet();
                ds.Tables.Add(CreateDirTable(di.Name));
            }

           UpdateIfNeeded(ds);
             ds.WriteXml(path + DirInfoFile, XmlWriteMode.WriteSchema);
        }
        else
            throw new ApplicationException("Unknown directory " + di.FullName);

        return ds;
    }

    private DataTable CreateDirTable(string artType)
    {
        DataTable tb = new DataTable(artType);
        tb.Columns.Add(new DataColumn("ID", typeof(string)));
        tb.Columns.Add(new DataColumn("VER", typeof(string)));
        tb.Columns.Add(new DataColumn("AI", typeof(string)));
        tb.Columns.Add(new DataColumn("VF", typeof(string)));
        tb.Columns.Add(new DataColumn("VT", typeof(string)));
        foreach (string lg in SupportedLanguages)
            tb.Columns.Add(new DataColumn("Name_" + lg, typeof(string)));
        tb.Columns.Add(new DataColumn("IF", typeof(bool)));
        tb.Columns.Add(new DataColumn("CO", typeof(string)));
        tb.Columns.Add(new DataColumn("CA", typeof(string)));
        tb.Columns.Add(new DataColumn("LU", typeof(string)));
        tb.Columns.Add(new DataColumn("UB", typeof(string)));
        tb.Columns.Add(new DataColumn("DI", typeof(string)));
        tb.Columns.Add(new DataColumn("AG", typeof(bool)));
        tb.Columns.Add(new DataColumn("CR", typeof(string)));

        CreateKeys(tb);

        return tb;

    }

    private void UpdateIfNeeded(DataSet ds)
    {
        DataTable tb = ds.Tables[0];

        bool oldStuff = tb.Columns[1].ColumnName == "Version";

        // New naming
        tb.Columns[0].ColumnName = "ID";
        tb.Columns[1].ColumnName = "VER";
        tb.Columns[2].ColumnName = "AI";
        tb.Columns[3].ColumnName = "VF";
        tb.Columns[4].ColumnName = "VT";


        if (tb.Columns.Count == 15)
        {
            tb.Columns.Add(new DataColumn("AG", typeof(bool)));
            tb.Columns["AG"].DefaultValue = false;
       }
        if (tb.Columns.Count == 16)
        {
            // Creation date if not exists update with last modif
            tb.Columns.Add(new DataColumn("CR", typeof(string)));
            foreach (DataRow row in tb.Rows)
                row["CR"] = DateTime.Parse(row["LU"].ToString()).ToString("yyyy-MM-dd");
        }


        // Update
        CreateKeys(tb);
        tb.AcceptChanges();

    }

    private void CreateKeys(DataTable tb)
    {
        DataColumn[] keys = new DataColumn[] { tb.Columns[0], tb.Columns[1], tb.Columns[2] };
        tb.PrimaryKey = keys;
    }

    #endregion //Load Artefact Lists

    #region Private Methods

    private DataRow GetRow(string artType, ArtefactPrimaryKey fullId)
    {
        DataRow row = null;
        DataSet ds = GetArtefactList(artType);
        if (ds != null)
            row = ds.Tables[0].Rows.Find(fullId.GetKey());

        return row;
    }

    private string GetString(object ob)
    {
        if (ob == DBNull.Value || ob == null)
            return string.Empty;
        return ob.ToString();
    }

    private void CreateRow(DataSet ds, ArtefactPrimaryKey key, TextTypeSdmx tts, string user, bool aggregated)
    {
        // Create row 
        DataRow newRow = ds.Tables[0].Rows.Add(new object[] {
                    key.Id, 
                    key.Version,
                    key.AgencyId,
                    });

        UpdateRow(newRow, key, tts, user, aggregated);

        // Default diffusion status at creation
        newRow["DI"] = ((int)DiffusionStatus.Intern).ToString();
        // Set date at creation
        newRow["CR"] = DateTime.Now.ToString("yyyy-MM-dd");
    }

    private void UpdateRow(DataRow row, ArtefactPrimaryKey key, TextTypeSdmx tts, string user, bool aggregated)
    {
        // Update ID (cases changes ?)
        row["ID"] = key.Id;

        // Update names
        int i = 0;
        foreach (string lg in SupportedLanguages)
            row["Name_" + lg] = tts.arrText[i++];

        // and validity
        row["VF"] = tts.ValidFrom == string.Empty ? null : tts.ValidFrom;
        row["VT"] = tts.ValidTo == string.Empty ? null : tts.ValidTo;

        // and IsFinal
        row["IF"] = tts.IsFinal;

        // and update date and updated by
        row["LU"] = DateTime.Now.ToString("s");
        row["UB"] = user;

        // and ensure no more check out
        row["CO"] = null;
        row["CA"] = null;

        row["AG"] = aggregated;
    }

    private void UpdateCheckOut(DataRow row, string user, bool checkout)
    {
        if (row != null)
        {
            if (checkout)
            {
                row["CO"] = user;
                row["CA"] = DateTime.Now.ToString("s");
            }
            else
            {
                row["CO"] = null;
                row["CA"] = null;
            }
        }
    }

    private void UpdateDiffusionStatus(DataRow row, string diffusionCd)
    {
        bool found = false;
        if (row != null)
        {
            foreach (int i in Enum.GetValues(typeof(DiffusionStatus)))
            {
                if (i.ToString() == diffusionCd)
                {
                    found = true;
                    row["DI"] = diffusionCd;
                    break;
                }
            }
        }

        if (!found)
            throw new ApplicationException("Invalid diffusion status code: " + diffusionCd);
    }

     
    #endregion //Private Methods

    #region Public Methods

    /// <summary>Is the submitted artefact type a supported type</summary>
    /// <param name="artType">submitted type</param>
    /// <returns>True if supported</returns>
    public bool ValidType(string artType)
    {
        return dicoList.ContainsKey(artType.ToLower());
    }

    /// <summary>Get the list of artefact types considered by the manager</summary>
    /// <returns>a list of all artefact types considered by the manager</returns>
    public List<string> GetArtefactTypes()
    {
        return new List<string>(dicoList.Keys);
    }

    /// <summary>Get information list about one type of artefact</summary>
    /// <param name="artType">the requested type of artefact</param>
    /// <returns>a DataSet holding the list</returns>
    public DataSet GetList(string artType)
    {
        DataSet dsList = new DataSet();

        // Wait until it is safe to enter.
        mut.WaitOne();

        try
        {
            // Process a copy to avoid clashing
            DataSet ds = GetArtefactList(artType);
            if (ds != null)
              dsList = ds.Copy();
        }
        finally
        {
            // Release the Mutex.
            mut.ReleaseMutex();
        }
        return dsList;
    }

    /// <summary>Retieve the list of all backups for an Artefact."</summary>
    /// <param name="arType">Type of artefact requested</param>
    /// <param name="fullId">Artefact key identification</param>
    /// <returns></returns>
    public DataSet GetArtefactBackupList(string arType, string fullId)
    {
        DataSet dsList = new DataSet();
        // Wait until it is safe to enter.
        mut.WaitOne();

        try
        {
            BackupManager bakMgr = new BackupManager(arType, fullId);
            dsList = bakMgr.GetArtefactBackupList(arType, fullId);
        }
        finally
        {
            // Release the Mutex.
            mut.ReleaseMutex();
        }
        return dsList;
    }

    /// <summary>Retieve as codelist the version updated according to required version."</summary>
    /// <param name="arType">Type of artefact requested</param>
    /// <param name="fullId">Artefact key identification</param>
    /// <returns></returns>
    public XmlDocument GetUpdateInfo(ArtefactPrimaryKey key)
    {
        XmlDocument xDoc = new XmlDocument();
        // Wait until it is safe to enter.
        mut.WaitOne();

        try
        {
            // Process a copy to avoid clashing
            DataSet ds = GetArtefactList(key.ArtefactType);
            if (ds != null && ds.Tables.Count > 0)
            {
                UpdateInfo ui = new UpdateInfo(ds.Tables[0]);
                xDoc = ui.GetUpdateInfoElement(key);
            }
        }
        finally
        {
            // Release the Mutex.
            mut.ReleaseMutex();
        }
        return xDoc;
    }


    /// <summary>Common methods for operations on artefact information list</summary>
    /// <returns>True if artefact identified in list (or new created)</returns>
    public bool UpdateList(ArtefactPrimaryKey key, ListOperation op, string user, 
        TextTypeSdmx tts, string diffCd, bool aggregated)
    {
        return UpdateList(key, op, user, tts, diffCd, aggregated, null, null, false);
    }

    /// <summary>Common methods for operations on artefact information list</summary>
    /// <returns>True if artefact identified in list (or new created)</returns>
    public bool UpdateList(ArtefactPrimaryKey key, ListOperation op, string user, 
        TextTypeSdmx tts, string diffCd, bool aggregated, string path, XmlDocument xDoc, bool doBackup)
    {
        DataRow targetRow = null;
        string coUser;
        string coTime;
        BackupManager bakMgr = null;

        // Wait until it is safe to enter.
        mut.WaitOne();

        try
        {
            // Locate 
            DataSet ds = GetArtefactList(key.ArtefactType);
            if (ds == null)
                throw new ApplicationException("Invalid Artefact Type: " + key.ArtefactType);

            targetRow = ds.Tables[0].Rows.Find(key.GetKey());
            if (targetRow != null)
            {
                // If OwnerShip support activated, check group owner authorization
                OwnerShipAuthorize(key, op, user, targetRow);
                                
                // Store check out User and Time
                coUser = GetString(targetRow["CO"]).ToLower();
                coTime = GetString( targetRow["CA"]);
                string theUser = user;

                switch (op)
                {
                    case ListOperation.Delete:
                        //  Ensure that if artefact is already 'Isfinal' only Administrators can operate
                        if (Manager.OwnerShip.IsUsed && Convert.ToBoolean(targetRow["IF"]) == true && Manager.OwnerShip.IsSystemAdmin(ref theUser) == false)
                            throw new ApplicationException(string.Format("Artefact {0} is finalized. Only Administrator is allowed to delete it!.", key.FullIdent));

                        // Delete entry even if checked out
                        ds.Tables[0].Rows.Remove(targetRow);

                        //Clean backup & keeps the one to delete as only backup
                        bakMgr = new BackupManager(path);
                        bakMgr.ClearAllBackup();
                        bakMgr.DoBackup();
                        break;

                    case ListOperation.CheckIn:
                        UpdateCheckOut(targetRow, user, false);
                        break;

                    case ListOperation.CheckOut:
                        // Only If already checked out by same or not check out
                        if (coUser.Length > 0)
                            if (coUser != user.ToLower())
                                throw new ApplicationException(string.Format("Artefact {0} is already checked out by: {1} ", key.FullIdent, coUser));

                        // Ensure that if artefact is already 'Isfinal' only Administrators can operate
                        if (Manager.OwnerShip.IsUsed && Convert.ToBoolean(targetRow["IF"]) == true && Manager.OwnerShip.IsSystemAdmin(ref theUser) == false)
                            throw new ApplicationException(string.Format("Artefact {0} is finalized. Only Administrator is allowed to modify it!.", key.FullIdent));

                        UpdateCheckOut(targetRow, user, true);
                        break;

                    case ListOperation.SetDiffStatus:
                        UpdateDiffusionStatus(targetRow, diffCd);
                        break;

                    case ListOperation.Update:
                        // Ensure not checked out by other
                        if (coUser != user.ToLower())
                        {
                            if (coUser.Length > 0)
                                throw new ApplicationException(string.Format("Artefact {0} is already checked out by: {1}", key.FullIdent, coUser));
                            else
                                throw new ApplicationException(string.Format("Artefact {0} is not checked out by: {1}", key.FullIdent, user));
                        }

                        // Do backup if requested
                        if (doBackup || BackupManager.BackupAutomatic)
                        {
                            bakMgr = new BackupManager(path);
                            bakMgr.DoBackup();
                        }

                        // Ensure file saved before operation on information list
                        xDoc.Save(path);

                        // Update information list row
                        UpdateRow(targetRow, key, tts, user, aggregated);
                        break;
                }
            }
            else
            {
                if (op != ListOperation.Update && op != ListOperation.Exist)
                    throw new ApplicationException("Unknown Artefact " + key.FullIdent);

                // Not found, the update is a creation
                if (op == ListOperation.Update)
                {
                    if(Manager.OwnerShip.IsUsed)
                        CheckOwnership(key, op, user);

                    // Ensure file saved before operation on information list
                    xDoc.Save(path);
                    CreateRow(ds, key, tts, user, aggregated);
                }
            }

            // Needs to persist updated information list
            if (op != ListOperation.Exist && op != ListOperation.Get)
            {
                ds.Tables[0].AcceptChanges();
                ds.WriteXml(_rootPath + key.ArtefactType + "\\" + DirInfoFile, XmlWriteMode.WriteSchema);
            }
        }

        catch (Exception ex)
        {
            if (ex is ApplicationException)
                throw;
            else
                throw new ApplicationException("Unable to execute command: " + op.ToString() + " for Artefact " + key.FullIdent);
        }

       
        finally
        {
            // Release the Mutex.
            mut.ReleaseMutex();
        }

        return targetRow != null;
    }

    public bool IsOwner(ArtefactPrimaryKey key, string user)
    {
        // this will raise exception in case of error
        CheckOwnership(key, ListOperation.Update, user);
        return true;
    }
    #endregion //Public Methods

    #region Ownership

    private void OwnerShipAuthorize(ArtefactPrimaryKey key, ListOperation op, string user, DataRow targetRow)
    {
        /*
         * Get          - If diffusion status is to none, only Registry administrators or member of group
         * Update       - Only Registry administrators or member of group
         * Exist        - no restriction
         * CheckOut     - Only Registry administrators or member of group
         * CheckIn      - Only Registry administrators or member of group that done the checkout
         * SetDiffStatus- Only Registry administrators or member of group that is Group Admin
         * Delete       - Only Registry administrators or member of group
         */
        if (Manager.OwnerShip.IsUsed && op != ListOperation.Exist)
        {
           
            if (op == ListOperation.Get)
            {
                if (targetRow != null && targetRow["DI"].ToString() == ((int)DiffusionStatus.No).ToString())
                {
                    if (string.IsNullOrEmpty(user))
                        throw new ApplicationException("This Artefact diffusion is reserved to it's owner's Group!");
                    else
                        CheckOwnership(key, op, user);
                }
            }
            else
            {
                CheckOwnership(key, op, user);
            }
        }
    }

    private void CheckOwnership(ArtefactPrimaryKey key, ListOperation op, string user)
    {
        string userId = user;
        if (Manager.OwnerShip.IsSystemAdmin(ref userId) == false)
        {
            if (Manager.OwnerShip.DicoOwnerGroup == null)
            {
                return;
            }

            // Is user member of Agency group
            if (Manager.OwnerShip.DicoOwnerGroup.ContainsKey(key.AgencyId) == false)
                throw new ApplicationException("Unknown Group in ownership managment: " + key.AgencyId);

            Group g = Manager.OwnerShip.DicoOwnerGroup[key.AgencyId];
            if (g.DicoUser.ContainsKey(userId) == false)
                throw new ApplicationException(string.Format("User {0} is not part of Agency Group {1}!", userId, key.AgencyId));

            if (op == ListOperation.SetDiffStatus)
            {
                User u = g.DicoUser[userId];
                if (u.Role != GroupUserRole.GroupAdmin)
                    throw new ApplicationException("Modification of diffusion Status is reserved to Group Administrator!");
            }
        }
    }
    #endregion

}
